﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Upreader.Application;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using System.Threading;
using Upreader.Application.Model;
using System.Diagnostics;
using Upreader.Usenet.Nntp.Encodings;

using IOStream = System.IO.Stream;

namespace Upreader.Engine
{
    public class FileAssembleProcessor
    {
        #region Fields

        private readonly DownloadEngineContext _context;

        private readonly Dictionary<int, FileAssembleState> _fileAssembleStateCollection;

        #endregion // Fields

        #region Constructor

        public FileAssembleProcessor(DownloadEngineContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");

            _context = context;

            _fileAssembleStateCollection = new Dictionary<int, FileAssembleState>();
        }

        #endregion // Constructor

        #region Methods

        public Task Run(CancellationToken cancellationToken)
        {
            return Task.Factory.StartNew(() =>
            {
                foreach (Segment segment in _context.CachedSegmentCollection.GetConsumingEnumerable(cancellationToken))
                {
                    int fileId = segment.FileId;

                    FileAssembleState fileAssembleState = GetOrCreateFileAssembleState(fileId);                

                    // add the current segment to the fileAssembleState for ease of use
                    fileAssembleState.PendingSegments.Add(segment);

                    while (fileAssembleState.PendingSegments.Any())
                    {
                        int nextSegmentIndexForAssembly = GetNextSegmentIndexForAssembly(fileId);
                        Segment nextSegment = fileAssembleState.PendingSegments.First();

                        if (nextSegment.Index == nextSegmentIndexForAssembly)
                        {
                            fileAssembleState.PendingSegments.Remove(nextSegment);

                            Trace.WriteLine(string.Format("Assembling segment {0} for {1}", nextSegment.Index, fileId), Resources.TraceCategory_File);

                            AssembleSegment(fileAssembleState, nextSegment);

                            // changes ar emade in AssembleSegment
                            _context.Service.UpdateSegment(nextSegment);

                            if (!fileAssembleState.PendingSegments.Any())
                            {
                                // this might be the last segment, check if there are more segments. If not, mark this file as completed
                                nextSegmentIndexForAssembly = GetNextSegmentIndexForAssembly(fileId);
                                if (nextSegmentIndexForAssembly == 0)  
                                {
                                    // 0 is the default, meaning nothing
                                    Trace.WriteLine(string.Format("Completed downloading {0} for operation {1}", fileAssembleState.File.FileName, fileAssembleState.File.DownloadId), Resources.TraceCategory_File);
                                    MarkFileCompleted(fileAssembleState.File);
                                }
                            }
                        }
                        else
                        {
                            // we have to wait for older segments to come in first, so break from the current loop
                            break;
                        }
                    }
                }
            }, cancellationToken, TaskCreationOptions.AttachedToParent | TaskCreationOptions.LongRunning, TaskScheduler.Default);
        }

        #endregion // Methods

        #region Helpers

        private void MarkFileCompleted(File file)
        {
            // search for broken segments, if found, mark the file as broken
            if (_context.Service.GetSegments(x => x.FileId == file.Id
                                               && x.State == SegmentState.AssembledBroken).Any())
            {
                file.State = FileState.Broken;
            }
            else
            {
                file.State = FileState.Downloaded;
            }

            _context.Service.UpdateFile(file);
        }

        private void AssembleSegment(FileAssembleState fileAssembleState, Segment segment)
        {
            IOStream segmentStream;
            try
            {
                // Load the segment from cache
                segmentStream = _context.SegmentDataStore.LeaseSegment(segment);
            }
            catch (InvalidOperationException)
            {
                // this segment was not found, schedule the segment for retry
                segmentStream = null;
                segment.State = SegmentState.Unhandled;
            }

            if (segmentStream != null)
            {
                bool isBroken = false;

                Type expectedStreamEncodingType;                
                switch (fileAssembleState.File.Encoding)
                {
                    case FileEncoding.Yenc:
                        expectedStreamEncodingType = typeof(YencStreamDecoder);
                        break;
                    case FileEncoding.UUEncode:
                        expectedStreamEncodingType = typeof(UUEncodeStreamDecoder);
                        break;
                    default:
                        expectedStreamEncodingType = null;
                        break;
                }

                // assemble this segment
                using (segmentStream)
                using (EncodedStream encodedStream = new EncodedStream(segmentStream, EncodedStream.DefaultStreamDecoders, expectedStreamEncodingType))
                {
                    YencEncodedStream yencEncodedStream = null;

                    // register a callback when an available decoder is found so we can apply some hacks for yenc (dirty bastard that it is)
                    encodedStream.AvailableDecoderFound += new EventHandler(delegate
                    {
                        yencEncodedStream = encodedStream.DecodeStream as YencEncodedStream;
                        if (yencEncodedStream != null)
                        {
                            // yEnc has this nasty implementation detail that one segment can end with an escape byte meaning the next segment should interpret its first byte as an escape byte.
                            yencEncodedStream.IsEscapedByte = segment.FirstByteIsYencEscaped;
                            // and mark the file as yenc encoded
                            fileAssembleState.File.Encoding = FileEncoding.Yenc;
                        }
                        else if (encodedStream.DecodeStream is UUEncodedStream)
                        {
                            // mark the file as UUEncoded
                            fileAssembleState.File.Encoding = FileEncoding.UUEncode;
                        }
                    });

                    _context.DownloadDataStore.WriteToFile(fileAssembleState.DownloadPath, fileAssembleState.File, encodedStream);

                    IEncodedStreamValidatable validatableStream = encodedStream.DecodeStream as IEncodedStreamValidatable;
                    // if the stream can be validated
                    if (validatableStream != null)
                    {
                        EncodedStreamValidatonResult validationResult = validatableStream.Validate();
                        if (validationResult == EncodedStreamValidatonResult.Invalid)
                        {
                            isBroken = true;
                        }
                    }
                }
                
                // cleanup resources
                _context.SegmentDataStore.CleanSegment(segment);

                // mark segment as assembled and optionally broken
                segment.State = isBroken ? SegmentState.AssembledBroken : SegmentState.Assembled;              
            }

            _context.Service.UpdateSegment(segment);
        }


        private int GetNextSegmentIndexForAssembly(int fileId)
        {
            return _context.Service.GetSegments(x => x.FileId == fileId 
                                                 && SegmentState.AssembledBroken != x.State
                                                 && SegmentState.Assembled != x.State
                                                 && SegmentState.Unavailable != x.State)
                                   .Select(x => x.Index)
                                   .OrderBy(x => x)
                                   .FirstOrDefault();
        }


        private FileAssembleState GetOrCreateFileAssembleState(int fileId)
        {
            // Find an existing fileAssembleState or create a new one if it doesnt exist yet
            FileAssembleState fileAssembleState;
            if (!_fileAssembleStateCollection.ContainsKey(fileId))
            {
                File file = _context.Service.GetFiles(x => x.Id == fileId).First();
                Download download = _context.Service.GetDownloads(x => x.Id == file.DownloadId).First();

                _context.DownloadDataStore.EnsureDownloadPath(_context.Configuration.DownloadStorageFolder, download);

                fileAssembleState = new FileAssembleState(file, download.DownloadPath);
                _fileAssembleStateCollection[fileId] = fileAssembleState;
            }
            else
            {
                fileAssembleState = _fileAssembleStateCollection[fileId];
            }
            return fileAssembleState;
        }


        #endregion // Helpers
    }
}
